﻿#region Copyright(C) 2018 FI Ltd. All rights reserved.

// ==================================================================================================
// Copyright(C) 2018 FI Ltd. All rights reserved.
//
// SYSTEM NAME	:   FI.Net Application Framework 
// COMPONENT ID :   FI.EntityFramework
// COMPONENT DESC:  
//
// CREATED DATE/BY: 18 Aug 2018 / Tian Mi
//
// REVISION HISTORY:	DATE/BY			SR#/CS/PM#/OTHERS		DESCRIPTION OF CHANGE
// ==================================================================================================

#endregion

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure.Interception;
using System.Linq;
using System.Linq.Expressions;

using FI.Core;
using FI.Core.ApplicationContexts;
using FI.Core.Utilities;
using System.Collections;
using DbCommandInterceptor = FI.EntityFramework.Interception.DbCommandInterceptor;
using System.Reflection;

namespace FI.EntityFramework
{
    /// <summary>
    /// The base class of <see cref="DbContext"/> specific Repository.
    /// </summary>
    /// <typeparam name="TDbContext">The type of the db context.</typeparam>
    public abstract class RepositoryBase<TDbContext> : IDisposable where TDbContext : DbContext
    {
        #region Fields & Properties
        private bool _isDisposed = false;
        private Dictionary<Type, IQueryable> _cachedQueryableSet = new Dictionary<Type, IQueryable>();
        private object _syncHelper = new object();
        private IDbInterceptor dbInterceptor;

        /// <summary>
        /// Gets the <see cref="DbContext"/>
        /// </summary>
        public TDbContext DbContext { get; private set; }
        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="RepositoryBase&lt;TDbContext&gt;"/> class.
        /// </summary>
        /// <param name="dbContext">The <see cref="DbContext"/></param>
        /// <param name="lazyLoadingEnabled">if set to <c>true</c> [lazy loading enabled].</param>
        /// <param name="interceptionEnabled">if set to <c>true</c> [inteception enabled].</param>
        public RepositoryBase(TDbContext dbContext, bool lazyLoadingEnabled = false, bool interceptionEnabled = false)
        {
            Guard.ArgumentNotNull(dbContext, "dbContext");

            dbContext.Configuration.LazyLoadingEnabled = lazyLoadingEnabled;
            this.DbContext = dbContext;
            if (interceptionEnabled)
            {
                this.dbInterceptor = new DbCommandInterceptor(dbContext);
                DbInterception.Add(this.dbInterceptor);
            }
        }

        #endregion

        #region Find<TEntity>
        /// <summary>
        /// Finds the entity by keys.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="keyValues">The key values.</param>
        /// <returns>The retrieved entity.</returns>
        public TEntity Find<TEntity>(object[] keyValues) where TEntity : class
        {
            TEntity entity = this.DbContext.Set<TEntity>().Find(keyValues);
            return entity;
        }
        #endregion

        #region GetAll<TEntity>

        /// <summary>
        /// Gets all entiies.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> GetAll<TEntity>() where TEntity : class
        {
            return GetDbSet<TEntity>();
        }

        /// <summary>
        /// Gets all entities.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TIncludeProperty">The type of the include property.</typeparam>
        /// <param name="includePath">The include path.</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> GetAll<TEntity, TIncludeProperty>(
                Expression<Func<TEntity, TIncludeProperty>> includePath)
            where TEntity : class
        {
            Guard.ArgumentNotNull(includePath, "includePath");

            return GetDbSet<TEntity>()
                .Include(includePath);
        }

        /// <summary>
        /// Gets all entities.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TIncludeProperty1">The type of the include property1.</typeparam>
        /// <typeparam name="TIncludeProperty2">The type of the include property2.</typeparam>
        /// <param name="includePath1">The include path1.</param>
        /// <param name="includePath2">The include path2.</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> GetAll<TEntity, TIncludeProperty1, TIncludeProperty2>(
                Expression<Func<TEntity, TIncludeProperty1>> includePath1,
                Expression<Func<TEntity, TIncludeProperty2>> includePath2)
            where TEntity : class
        {
            Guard.ArgumentNotNull(includePath1, "includePath1");
            Guard.ArgumentNotNull(includePath2, "includePath2");

            return GetDbSet<TEntity>()
                .Include(includePath1)
                .Include(includePath2);
        }

        /// <summary>
        /// Gets all entities.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TIncludeProperty1">The type of the include property1.</typeparam>
        /// <typeparam name="TIncludeProperty2">The type of the include property2.</typeparam>
        /// <typeparam name="TIncludeProperty3">The type of the include property3.</typeparam>
        /// <param name="includePath1">The include path1.</param>
        /// <param name="includePath2">The include path2.</param>
        /// <param name="includePath3">The include path3.</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> GetAll<TEntity, TIncludeProperty1, TIncludeProperty2, TIncludeProperty3>(
                Expression<Func<TEntity, TIncludeProperty1>> includePath1,
                Expression<Func<TEntity, TIncludeProperty2>> includePath2,
                Expression<Func<TEntity, TIncludeProperty3>> includePath3)
            where TEntity : class
        {
            Guard.ArgumentNotNull(includePath1, "includePath1");
            Guard.ArgumentNotNull(includePath2, "includePath2");
            Guard.ArgumentNotNull(includePath3, "includePath3");

            return GetDbSet<TEntity>()
                .Include(includePath1)
                .Include(includePath2)
                .Include(includePath3);
        }

        #endregion

        #region Get<TEntity>

        /// <summary>
        /// Gets entities by the specified predicate.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> Get<TEntity>(
                Expression<Func<TEntity, bool>> predicate)
            where TEntity : class
        {
            Guard.ArgumentNotNull(predicate, "predicate");

            return GetDbSet<TEntity>()
                .Where(predicate);
        }

        /// <summary>
        /// Gets entities by the specified predicate.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TIncludeProperty">The type of the Include property.</typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <param name="includePath">The include path.</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> Get<TEntity, TIncludeProperty>(
                Expression<Func<TEntity, bool>> predicate,
                Expression<Func<TEntity, TIncludeProperty>> includePath)
            where TEntity : class
        {
            Guard.ArgumentNotNull(predicate, "predicate");

            return GetDbSet<TEntity>()
                .Where(predicate)
                .Include(includePath);
        }

        /// <summary>
        /// Gets entities by the specified predicate.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TIncludeProperty1">The type of the Include property1.</typeparam>
        /// <typeparam name="TIncludeProperty2">The type of the Include property2.</typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <param name="includePath1">The include path1.</param>
        /// <param name="includePath2">The include path2.</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> Get<TEntity, TIncludeProperty1, TIncludeProperty2>(
                Expression<Func<TEntity, bool>> predicate,
                Expression<Func<TEntity, TIncludeProperty1>> includePath1,
                Expression<Func<TEntity, TIncludeProperty2>> includePath2)
            where TEntity : class
        {
            Guard.ArgumentNotNull(predicate, "predicate");
            Guard.ArgumentNotNull(includePath1, "includePath1");
            Guard.ArgumentNotNull(includePath2, "includePath2");

            return GetDbSet<TEntity>()
                .Where(predicate)
                .Include(includePath1)
                .Include(includePath2);
        }

        /// <summary>
        /// Gets the specified predicate.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TIncludeProperty1">The type of the Include property1.</typeparam>
        /// <typeparam name="TIncludeProperty2">The type of the Include property2.</typeparam>
        /// <typeparam name="TIncludeProperty3">The type of the Include property3.</typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <param name="includePath1">The include path1.</param>
        /// <param name="includePath2">The include path2.</param>
        /// <param name="includePath3">The include path3.</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> Get<TEntity, TIncludeProperty1, TIncludeProperty2, TIncludeProperty3>(
                Expression<Func<TEntity, bool>> predicate,
                Expression<Func<TEntity, TIncludeProperty1>> includePath1,
                Expression<Func<TEntity, TIncludeProperty2>> includePath2,
                Expression<Func<TEntity, TIncludeProperty3>> includePath3)
            where TEntity : class
        {
            Guard.ArgumentNotNull(predicate, "predicate");
            Guard.ArgumentNotNull(includePath1, "includePath1");
            Guard.ArgumentNotNull(includePath2, "includePath2");
            Guard.ArgumentNotNull(includePath3, "includePath3");

            return GetDbSet<TEntity>()
                .Where(predicate)
                .Include(includePath1)
                .Include(includePath2)
                .Include(includePath3);
        }
        #endregion

        #region GetPagingData
        /// <summary>
        /// Gets entities by the specified predicate.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <param name="pageSize">Size of the page.</param>
        /// <param name="pageIndex">Index of the page.</param>
        /// <param name="sortColumn">The sort column.</param>
        /// <param name="isAsc">if set to <c>true</c> [is asc].</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> GetPageableData<TEntity>(
            Expression<Func<TEntity, bool>> predicate,
            int pageSize,
            int pageIndex,
            string sortColumn,
            bool isAsc) where TEntity : class
        {
            Guard.ArgumentNotNull(predicate, "predicate");
            Guard.ArgumentNotNull(sortColumn, "sortColumn");

            return GetDbSet<TEntity>().Where(predicate)
                .GetPageableData(pageSize, pageIndex, sortColumn, isAsc);
        }

        /// <summary>
        /// Gets entities by the specified predicate.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TIncludeProperty">The type of the include property.</typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <param name="pageSize">Size of the page.</param>
        /// <param name="pageIndex">Index of the page.</param>
        /// <param name="sortColumn">The sort column.</param>
        /// <param name="isAsc">if set to <c>true</c> [is asc].</param>
        /// <param name="includePath">The include path.</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> GetPageableData<TEntity, TIncludeProperty>(
            Expression<Func<TEntity, bool>> predicate,
            int pageSize,
            int pageIndex,
            string sortColumn,
            bool isAsc,
            Expression<Func<TEntity, TIncludeProperty>> includePath) where TEntity : class
        {
            Guard.ArgumentNotNull(predicate, "predicate");
            Guard.ArgumentNotNull(sortColumn, "sortColumn");
            Guard.ArgumentNotNull(includePath, "includePath");

            return GetDbSet<TEntity>().Where(predicate)
                .Include(includePath)
                .GetPageableData(pageSize, pageIndex, sortColumn, isAsc);
        }

        /// <summary>
        /// Gets entities by the specified predicate.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TIncludeProperty1">The type of the include property1.</typeparam>
        /// <typeparam name="TIncludeProperty2">The type of the include property2.</typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <param name="pageSize">Size of the page.</param>
        /// <param name="pageIndex">Index of the page.</param>
        /// <param name="sortColumn">The sort column.</param>
        /// <param name="isAsc">if set to <c>true</c> [is asc].</param>
        /// <param name="includePath1">The include path1.</param>
        /// <param name="includePath2">The include path2.</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> GetPageableData<TEntity, TIncludeProperty1, TIncludeProperty2>(
            Expression<Func<TEntity, bool>> predicate,
            int pageSize,
            int pageIndex,
            string sortColumn,
            bool isAsc,
            Expression<Func<TEntity, TIncludeProperty1>> includePath1,
            Expression<Func<TEntity, TIncludeProperty2>> includePath2) where TEntity : class
        {
            Guard.ArgumentNotNull(predicate, "predicate");
            Guard.ArgumentNotNull(sortColumn, "sortColumn");
            Guard.ArgumentNotNull(includePath1, "includePath1");
            Guard.ArgumentNotNull(includePath2, "includePath2");

            return GetDbSet<TEntity>().Where(predicate)
                .Include(includePath1).Include(includePath2)
                .GetPageableData(pageSize, pageIndex, sortColumn, isAsc);
        }

        /// <summary>
        /// Gets entities by the specified predicate.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TIncludeProperty1">The type of the include property1.</typeparam>
        /// <typeparam name="TIncludeProperty2">The type of the include property2.</typeparam>
        /// <typeparam name="TIncludeProperty3">The type of the include property3.</typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <param name="pageSize">Size of the page.</param>
        /// <param name="pageIndex">Index of the page.</param>
        /// <param name="sortColumn">The sort column.</param>
        /// <param name="isAsc">if set to <c>true</c> [is asc].</param>
        /// <param name="includePath1">The include path1.</param>
        /// <param name="includePath2">The include path2.</param>
        /// <param name="includePath3">The include path3.</param>
        /// <returns>The retrieved entities.</returns>
        public IEnumerable<TEntity> GetPageableData<TEntity, TIncludeProperty1, TIncludeProperty2, TIncludeProperty3>(
            Expression<Func<TEntity, bool>> predicate,
            int pageSize,
            int pageIndex,
            string sortColumn,
            bool isAsc,
            Expression<Func<TEntity, TIncludeProperty1>> includePath1,
            Expression<Func<TEntity, TIncludeProperty1>> includePath2,
            Expression<Func<TEntity, TIncludeProperty1>> includePath3) where TEntity : class
        {
            Guard.ArgumentNotNull(predicate, "predicate");
            Guard.ArgumentNotNull(sortColumn, "sortColumn");
            Guard.ArgumentNotNull(includePath1, "includePath1");
            Guard.ArgumentNotNull(includePath2, "includePath2");
            Guard.ArgumentNotNull(includePath3, "includePath3");

            return GetDbSet<TEntity>().Where(predicate)
                .Include(includePath1).Include(includePath2).Include(includePath3)
                .GetPageableData(pageSize, pageIndex, sortColumn, isAsc);
        }
        #endregion

        #region Save
        /// <summary>
        /// Saves the specified entity.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="entity">The entity.</param>
        public void Save<TEntity>(TEntity entity) where TEntity : class
        {
            Guard.ArgumentNotNull(entity, "entity");

            try
            {
                var propertyInfo = entity.GetType().GetProperty(EntityFrameworkConfigurationView.DataStateField);
                if (propertyInfo == null) throw new EntityFrameworkException("There is no 'State' property.");

                EntityState state = (EntityState)(propertyInfo.GetValue(entity));
                switch (state)
                {
                    case EntityState.Added:
                        MarkAsAdded(entity);
                        MarkSubEntities(entity, null, EntityState.Added);
                        break;
                    case EntityState.Modified:
                        MarkAsModified(entity);
                        MarkSubEntities(entity, null);
                        break;
                    case EntityState.Deleted:
                        MarkSubEntities(entity, null, EntityState.Deleted);
                        MarkAsDeleted(entity);
                        break;
                    default:
                        MarkSubEntities(entity, null);
                        break;
                }

                this.DbContext.SaveChanges();
            }
            catch (Exception ex)
            {
                ExceptionHelper.HandleCommonDbException(ex);
                throw;
            }
        }

        /// <summary>
        /// Marks as added.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="entity">The entity.</param>
        public void MarkAsAdded<TEntity>(TEntity entity) where TEntity : class
        {
            Guard.ArgumentNotNull(entity, "entity");

            var entry = this.DbContext.Entry<TEntity>(entity);
            entry.State = EntityState.Added;

            entry.Property<int>(EntityFrameworkConfigurationView.VersionNoField).CurrentValue = 1;
            entry.Property<string>(EntityFrameworkConfigurationView.TransactionIdField).CurrentValue = ApplicationContext.Current.TransactionId;
            entry.Property<string>(EntityFrameworkConfigurationView.CreatedByField).CurrentValue = ApplicationContext.Current.UserName;
            entry.Property<string>(EntityFrameworkConfigurationView.LastUpdatedByField).CurrentValue = ApplicationContext.Current.UserName;
            entry.Property<DateTime>(EntityFrameworkConfigurationView.CreatedTimeField).CurrentValue = DateTime.Now;
            entry.Property<DateTime>(EntityFrameworkConfigurationView.LastUpdatedTimeField).CurrentValue = DateTime.Now;
        }

        private void SetStringPropertyEmptyString<TEntity>(TEntity entity) where TEntity : class
        {
            foreach (var propertyInfo in entity.GetType().GetProperties())
            {
                if (propertyInfo.PropertyType == typeof(string) && propertyInfo.GetValue(entity) == null)
                {
                    propertyInfo.SetValue(entity, "");
                }
            }
        }

        /// <summary>
        /// Sets the property value.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="property">The property.</param>
        /// <param name="value">The value.</param>
        public void SetPropertyValue<TEntity, TProperty>(TEntity entity, Expression<Func<TEntity, TProperty>> property, TProperty value) where TEntity : class
        {
            var entry = this.DbContext.Entry<TEntity>(entity);
            entry.Property(property).CurrentValue = value;
            if (!entry.Property(property).IsModified)
            {
                entry.Property(property).IsModified = true;
            }
        }

        /// <summary>
        /// Sets the property value.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="entity">The entity.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="value">The value.</param>
        public void SetPropertyValue<TEntity, TProperty>(TEntity entity, string propertyName, TProperty value) where TEntity : class
        {
            var entry = this.DbContext.Entry<TEntity>(entity);
            entry.Property(propertyName).CurrentValue = value;
            if (!entry.Property(propertyName).IsModified)
            {
                entry.Property(propertyName).IsModified = true;
            }
        }

        /// <summary>
        /// Marks as modified partial.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="entity">The entity.</param>
        public void MarkAsModifiedPartial<TEntity>(TEntity entity, params Expression<Func<TEntity, object>>[] propertiesToUpdate) where TEntity : class
        {
            Guard.ArgumentNotNull(entity, "entity");

            SetStringPropertyEmptyString(entity);

            this.DbContext.Set<TEntity>().Attach(entity);

            var entry = this.DbContext.Entry<TEntity>(entity);

            foreach (var p in propertiesToUpdate)
            {
                DbContext.Entry(entity).Property(p).IsModified = true;
            }

            int currentVersionNo = entry.Property<int>(EntityFrameworkConfigurationView.VersionNoField).CurrentValue;
            entry.Property<int>(EntityFrameworkConfigurationView.VersionNoField).OriginalValue = currentVersionNo;
            entry.Property<int>(EntityFrameworkConfigurationView.VersionNoField).CurrentValue = currentVersionNo + 1;

            entry.Property<string>(EntityFrameworkConfigurationView.TransactionIdField).CurrentValue = ApplicationContext.Current.TransactionId;
            entry.Property<string>(EntityFrameworkConfigurationView.LastUpdatedByField).CurrentValue = ApplicationContext.Current.UserName;
            entry.Property<DateTime>(EntityFrameworkConfigurationView.LastUpdatedTimeField).CurrentValue = DateTime.Now;
        }

        /// <summary>
        /// Marks as modified.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="entity">The entity.</param>
        public void MarkAsModified<TEntity>(TEntity entity) where TEntity : class
        {
            Guard.ArgumentNotNull(entity, "entity");

            SetStringPropertyEmptyString(entity);

            this.DbContext.Set<TEntity>().Attach(entity);

            var entry = this.DbContext.Entry<TEntity>(entity);
            entry.State = EntityState.Modified;

            int currentVersionNo = entry.Property<int>(EntityFrameworkConfigurationView.VersionNoField).CurrentValue;
            entry.Property<int>(EntityFrameworkConfigurationView.VersionNoField).OriginalValue = currentVersionNo;
            entry.Property<int>(EntityFrameworkConfigurationView.VersionNoField).CurrentValue = currentVersionNo + 1;

            entry.Property<string>(EntityFrameworkConfigurationView.TransactionIdField).CurrentValue = ApplicationContext.Current.TransactionId;
            entry.Property<string>(EntityFrameworkConfigurationView.LastUpdatedByField).CurrentValue = ApplicationContext.Current.UserName;
            entry.Property<DateTime>(EntityFrameworkConfigurationView.LastUpdatedTimeField).CurrentValue = DateTime.Now;

            entry.Property<string>(EntityFrameworkConfigurationView.CreatedByField).IsModified = false;
            entry.Property<DateTime>(EntityFrameworkConfigurationView.CreatedTimeField).IsModified = false;

            //var values = entry.GetDatabaseValues();
            //entry.Property<string>(EntityFrameworkConfigurationView.CreatedByField).CurrentValue = values.GetValue<string>(EntityFrameworkConfigurationView.CreatedByField);
            //entry.Property<DateTime>(EntityFrameworkConfigurationView.CreatedTimeField).CurrentValue = values.GetValue<DateTime>(EntityFrameworkConfigurationView.CreatedTimeField);
        }

        /// <summary>
        /// Marks as deleted.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="entity">The entity.</param>
        public void MarkAsDeleted<TEntity>(TEntity entity) where TEntity : class
        {
            Guard.ArgumentNotNull(entity, "entity");

            var entry = this.DbContext.Entry<TEntity>(entity);
            entry.State = EntityState.Deleted;

            entry.Property<string>(EntityFrameworkConfigurationView.TransactionIdField).OriginalValue = ApplicationContext.Current.TransactionId;
            entry.Property<string>(EntityFrameworkConfigurationView.LastUpdatedByField).OriginalValue = ApplicationContext.Current.UserName;
            entry.Property<DateTime>(EntityFrameworkConfigurationView.LastUpdatedTimeField).OriginalValue = DateTime.Now;
        }

        private void MarkSubEntities<TEntity>(TEntity entity, Type parentType, EntityState markState = 0) where TEntity : class
        {
            Guard.ArgumentNotNull(entity, "entity");

            foreach (var propertyInfo in entity.GetType().GetProperties())
            {
                if (propertyInfo.PropertyType.IsGenericType)
                {
                    IEnumerable subEntityEnumerable = propertyInfo.GetValue(entity) as IEnumerable;
                    if (subEntityEnumerable == null) continue;

                    List<object> listSubEntity = new List<object>();
                    foreach (var subEntity in subEntityEnumerable)
                    {
                        listSubEntity.Add(subEntity);
                    }

                    foreach (var subEntity in listSubEntity)
                    {
                        if (markState == 0)
                        {
                            var pi = subEntity.GetType().GetProperty(EntityFrameworkConfigurationView.DataStateField);
                            if (pi == null) throw new EntityFrameworkException("There is no 'State' property.");

                            EntityState state = (EntityState)(pi.GetValue(subEntity));
                            switch (state)
                            {
                                case EntityState.Added:
                                    MarkAsAdded(subEntity);
                                    MarkSubEntities(subEntity, entity.GetType(), EntityState.Added);
                                    break;
                                case EntityState.Modified:
                                    MarkAsModified(subEntity);
                                    MarkSubEntities(subEntity, entity.GetType());
                                    break;
                                case EntityState.Deleted:
                                    MarkSubEntities(subEntity, entity.GetType(), EntityState.Deleted);
                                    MarkAsDeleted(subEntity);
                                    break;
                                default:
                                    MarkSubEntities(subEntity, entity.GetType());
                                    break;
                            }
                        }
                        else
                        {
                            switch (markState)
                            {
                                case EntityState.Added:
                                    MarkAsAdded(subEntity);
                                    MarkSubEntities(subEntity, entity.GetType(), EntityState.Added);
                                    break;
                                case EntityState.Deleted:
                                    MarkSubEntities(subEntity, entity.GetType(), EntityState.Deleted);
                                    MarkAsDeleted(subEntity);
                                    break;
                            }
                        }
                    }
                }
                else if (propertyInfo.PropertyType.IsClass && propertyInfo.PropertyType != typeof(string) &&
                    (parentType == null || parentType != propertyInfo.PropertyType))
                {
                    var relatedEntity = propertyInfo.GetValue(entity);
                    if (relatedEntity == null) continue;

                    if (markState == 0)
                    {
                        var pi = relatedEntity.GetType().GetProperty(EntityFrameworkConfigurationView.DataStateField);
                        if (pi == null) throw new EntityFrameworkException("There is no 'State' property.");

                        EntityState state = (EntityState)(pi.GetValue(relatedEntity));
                        switch (state)
                        {
                            case EntityState.Added:
                                MarkAsAdded(relatedEntity);
                                break;
                            case EntityState.Modified:
                                MarkAsModified(relatedEntity);
                                break;
                            case EntityState.Deleted:
                                MarkAsDeleted(relatedEntity);
                                break;
                        }
                    }
                    else
                    {
                        switch (markState)
                        {
                            case EntityState.Added:
                                MarkAsAdded(relatedEntity);
                                break;
                            case EntityState.Deleted:
                                MarkAsDeleted(relatedEntity);
                                break;
                        }
                    }
                }
            }
        }
        #endregion

        #region IDisposable

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <param name="disposing">True/false</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                if (disposing)
                {
                    if (null != this.dbInterceptor)
                    {
                        DbInterception.Remove(this.dbInterceptor);
                    }
                    this.DbContext.Dispose();
                }
                _isDisposed = true;
            }
        }

        #endregion

        #region Private Methods
        private IQueryable<TEntity> GetDbSet<TEntity>() where TEntity : class
        {
            return GetQueryableSet<TEntity>();
        }

        /// <summary>
        /// Gets the queryable set.
        /// </summary>
        /// <typeparam name="TEntity">The type of the T entity.</typeparam>
        /// <returns>IQueryable{``0}.</returns>
        /// <exception cref="NotImplementedException"></exception>
        private IQueryable<TEntity> GetQueryableSet<TEntity>() where TEntity : class
        {
            try
            {
                IQueryable queryableSet;
                if (_cachedQueryableSet.TryGetValue(typeof(TEntity), out queryableSet))
                {
                    return (IQueryable<TEntity>)queryableSet;
                }
                lock (_syncHelper)
                {
                    if (_cachedQueryableSet.TryGetValue(typeof(TEntity), out queryableSet))
                    {
                        return (IQueryable<TEntity>)queryableSet;
                    }
                    else
                    {
                        DbSet<TEntity> objectSet = this.DbContext.Set<TEntity>();
                        _cachedQueryableSet.Add(typeof(TEntity), objectSet);
                        return objectSet;
                    }
                }
            }
            catch (Exception ex)
            {
                ExceptionHelper.HandleCommonDbException(ex);
                throw;
            }
        }
        #endregion
    }

    #region DynamicQueryableExtensions
    /// <summary>
    /// The Dynamic Queryable Extension class.
    /// </summary>
    public static class DynamicQueryableExtensions
    {
        /// <summary>
        /// Gets the paging data.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="source">The source.</param>
        /// <param name="pageSize">Size of the page.</param>
        /// <param name="pageIndex">Index of the page.</param>
        /// <param name="sortColumn">The sort column.</param>
        /// <param name="isAsc">if set to <c>true</c> [is asc].</param>
        /// <returns></returns>
        public static IEnumerable<TEntity> GetPageableData<TEntity>(this IQueryable<TEntity> source,
            int pageSize,
            int pageIndex,
            string sortColumn,
            bool isAsc) where TEntity : class
        {
            return source.OrderBy(sortColumn, isAsc).Skip(pageSize * (pageIndex - 1)).Take(pageSize);
        }

        /// <summary>
        /// Orders the by.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="source">The source.</param>
        /// <param name="sortColumn">The sort column.</param>
        /// <param name="isAsc">if set to <c>true</c> [is asc].</param>
        /// <returns></returns>
        public static IEnumerable<TEntity> OrderBy<TEntity>(this IEnumerable<TEntity> source, string sortColumn, bool isAsc) where TEntity : class
        {
            if (!string.IsNullOrEmpty(sortColumn))
            {
                ParameterExpression param = Expression.Parameter(typeof(TEntity), "i");
                MemberExpression body = Expression.PropertyOrField(param, sortColumn);

                Expression conversion = Expression.Convert(body, typeof(object));

                var sortExpression = Expression.Lambda<Func<TEntity, object>>(conversion, param);

                //apply sort
                source = isAsc ? source.OrderBy(sortExpression.Compile()) : source.OrderByDescending(sortExpression.Compile());
            }

            return source;
        }

        /// <summary>
        /// Sorts the specified source.
        /// </summary>
        /// <typeparam name="TSource">The type of the source.</typeparam>
        /// <param name="source">The source.</param>
        /// <param name="orderBy">The order by.</param>
        /// <param name="sortDirection">The sort direction.</param>
        /// <returns></returns>
        public static IOrderedEnumerable<TSource> Sort<TSource>(this IEnumerable<TSource> source, string orderBy, SortDirection sortDirection)
        {
            var sourceType = typeof(TSource);
            var propertyInfo = sourceType.GetProperty(orderBy, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
            if (propertyInfo == null)
            {
                var message = String.Format("'{0}' does not contain a definition for '{1}'.", sourceType.Name, orderBy);
                throw new ArgumentException(message, "orderBy");
            }

            var parameterExpression = Expression.Parameter(sourceType);
            var bodyExpression = Expression.Property(parameterExpression, propertyInfo);
            var selecterExpression = Expression.Lambda(bodyExpression, parameterExpression);

            var methodName = (sortDirection == SortDirection.Ascending) ? "OrderBy" : "OrderByDescending";
            var methodExpression = Expression.Call(typeof(Enumerable), methodName, new Type[] { sourceType, propertyInfo.PropertyType }, Expression.Constant(source), selecterExpression);
            return (IOrderedEnumerable<TSource>)methodExpression.Method.Invoke(null, new object[] { source, selecterExpression.Compile() });
        }

        /// <summary>
        /// Sorts the specified source.
        /// </summary>
        /// <typeparam name="TSource">The type of the source.</typeparam>
        /// <param name="source">The source.</param>
        /// <param name="orderBy">The order by.</param>
        /// <param name="sortDirection">The sort direction.</param>
        /// <returns></returns>
        public static IOrderedQueryable<TSource> Sort<TSource>(this IQueryable<TSource> source, string orderBy, SortDirection sortDirection)
        {
            var sourceType = typeof(TSource);
            var propertyInfo = sourceType.GetProperty(orderBy, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
            if (propertyInfo == null)
            {
                var message = String.Format("'{0}' does not contain a definition for '{1}'.", sourceType.Name, orderBy);
                throw new ArgumentException(message, "orderBy");
            }

            var parameterExpression = Expression.Parameter(sourceType);
            var bodyExpression = Expression.Property(parameterExpression, propertyInfo);
            var selecterExpression = Expression.Lambda(bodyExpression, parameterExpression);

            var methodName = (sortDirection == SortDirection.Ascending) ? "OrderBy" : "OrderByDescending";
            var methodExpression = Expression.Call(typeof(Queryable), methodName, new Type[] { sourceType, propertyInfo.PropertyType }, Expression.Constant(source), selecterExpression);
            return (IOrderedQueryable<TSource>)methodExpression.Method.Invoke(null, new object[] { source, selecterExpression });
        }
    }
    #endregion
}
